const Pencil = require('./pencil');
const Dom = require('./../../lib/Dom');
const PencilNamespaces = require('./../../lib/Namespaces');

export default class Canvas extends HTMLElement {

    protected element: HTMLElement;

    public xferHelpers: any[] = [];

    protected popup: HTMLElement;
    protected svg: SVGElement;

    public width: number;
    public height: number;

    protected oldElement;
    protected dragObservers: any[];
    protected spaceHeld: boolean;
    protected painterPropertyMap: string[];
    protected duplicateMode: boolean;
    protected mouseUp: boolean;
    protected lockPointerFunction: Function;
    protected autoScrollTimout: any;
    protected options: any;
    protected focusableBox: Node;
    protected arrangementMenu: HTMLElement;
    protected linkMenu: HTMLElement;
    protected attachToMenu: HTMLElement;
    protected undoContextMenu: HTMLElement;
    protected redoContextMenu: HTMLElement;
    protected popupSeparator: HTMLElement;
    protected propertyMenuItem: HTMLElement;
    protected lockingMenuItem: HTMLElement;
    protected topGroup: HTMLElement;
    protected drawingLayer: HTMLElement;
    protected controlLayer: HTMLElement;
    protected selectionContainer: HTMLElement;
    protected rangeBoundRect: HTMLElement;
    protected bgLayer: HTMLElement;
    protected backgroundImage: HTMLElement;
    protected hasBackgroundImage: boolean;
    protected snappingHelper: SnappingHelper;
    protected idSeed: number;
    protected onScreenEditors: any[];
    protected movementDisabled: boolean;
    protected lastMouse: { x: number, y: number};
    protected zoom: number;
    private _lastPX: number;
    private _currentPX: number;
    private _lastPY: number;
    private _currentPY: number;
    private _lastScrollX: number;
    private _lastScrollY: number;
    private _scrollPane: HTMLElement;
    private controllerHeld: boolean;
    private currentController: any;
    protected careTaker: CanvasCareTaker;
    protected lastTop: SVGElement;
    protected hasMoved: boolean;
    protected resizing: boolean;
    protected isSelectingRange: boolean;
    protected reClick: boolean;

    protected resizeInfoLabel: HTMLElement;
    protected propertyPageEditor: HTMLElement;
    protected contextMenuEditor: HTMLElement;
    protected currentRange: any;

    protected showResizerTimeout: number;
    protected _button: number;
    protected oX: number;
    protected oY: number;

    protected dragOverlay: HTMLElement;

    protected lastMousePos: {x: number, y: number};


    protected lockingStatus: {
        controller?: any,
        node?: any,
    };

    protected resizeInfo: {
        lastSize: { w: number, h: number },
        ox: number, oy: number, ow: number, oh: number
    };

    protected resizer: {
        offsetWidth: number,
        offsetHeight: number,
    };

    protected duplicateFunc: Function;

    protected readonly ownerDocument = {
        get: function () {
            return this.element.ownerDocument;
        }
    }

    public constructor(element, options, containerScrollPane) {
        super();

        this.element = element;
        this.oldElement = "";
        this.__delegate("addEventListener", "hasAttribute", "getAttribute", "setAttribute", "setAttributeNS", "removeAttribute", "removeAttributeNS", "dispatchEvent");
        this.element.setAttribute("pencil-canvas", "true");
        this.element.parentNode.setAttribute("tabindex", "0");

        this.xferHelpers = [];
        this.dragObservers = [];

        this.spaceHeld = false;
        this.painterPropertyMap = this.getPainterPropertyMap();
        this.duplicateMode = false;
        this.mouseUp = false;
        this.width;
        this.height;
        // building the content as: box >> svg
        var thiz = this;
        this.lockPointerFunction = null;
        this.autoScrollTimout = null;

        this.options = options || {};

        this.focusableBox = this.element.parentNode;

        this.addEventListener("mousedown", function (event) {
            var inDrawing = Dom.findUpward(event.originalTarget, function (node) {

                return (node == thiz.svg);
            });
            if (!inDrawing) {
                thiz.clearSelection();
                thiz.selectNone();
            }

        }, false);

        this.addEventListener("mouseup", function (event) {
            if (thiz.duplicateMode) {
                thiz.mouseUp = true;
                thiz.duplicateMode = null;
                if (this.controllerHeld && this.currentController
                    && this.currentController.markAsMoving) thiz.finishMoving(event);
            }
        }, false);

        // create the context menu
        this.popup = this.createElementByName("contextPopup");
        this.arrangementMenu = this.createElementByName("arrangementMenu");
        this.linkMenu = this.createElementByName("linkMenu");
        this.arrangementMenu._canvas = this;

        this.undoContextMenu = this.createElementByName("contextUndoMenu");
        this.redoContextMenu = this.createElementByName("contextRedoMenu");

        this.popupSeparator = this.createElementByName("contextPopupSeparator");
        var id = "popup" + Math.round(1000 * Math.random());
        this.popup.setAttribute("id", id);

        this.propertyMenuItem = this.createElementByName("propertyMenuItem");
        this.lockingMenuItem = this.createElementByName("lockingMenuItem");

        this.svg = document.createElementNS(PencilNamespaces.svg, "svg:svg");
        this.svg.setAttribute("version", "1.0");

        // FIXME: this will not be hard-coded
        this.width = this.hasAttribute("width") ? parseInt(this
            .getAttribute("width"), 10) : this.element.offsetWidth;
        this.height = this.hasAttribute("height") ? parseInt(this
            .getAttribute("height"), 10) : this.element.offsetHeight;
        this.element.appendChild(this.svg);

        this.topGroup = document.createElementNS(PencilNamespaces.svg, "svg:g");

        var fixTarget = this.topGroup;
        var fixRef = this.svg;

        // window.setTimeout(function () {
        //     try {
        //         var matrix = fixTarget.getScreenCTM();
        //         var dx = matrix.e - Math.floor(matrix.e);
        //         var dy = matrix.f - Math.floor(matrix.f);
        //
        //         if ((dx != 0 || dy != 0) && thiz.zoom == 1) {
        //             matrix = fixRef.getScreenCTM();
        //             dx = (matrix.e - Math.floor(matrix.e)) / thiz.zoom;
        //             dy = (matrix.f - Math.floor(matrix.f)) / thiz.zoom;
        //
        //             var t = "translate(" + (0 - dx) + "," + (0 - dy) + ")";
        //            fixTarget.setAttribute("transform", t);
        //         }
        //     } catch (e) {
        //     }
        //     window.setTimeout(arguments.callee, 1000);
        // }, 1000);

        this.svg.appendChild(this.topGroup);

        // create the background layer
        this.bgLayer = document.createElementNS(PencilNamespaces.svg, "svg:g");
        this.bgLayer.setAttributeNS(PencilNamespaces.p, "p:name", "Background");
        var rect = document.createElementNS(PencilNamespaces.svg, "svg:rect");
        rect.setAttribute("x", "0");
        rect.setAttribute("y", "0");
        rect.setAttribute("width", "2000");
        rect.setAttribute("height", "2000");
        rect.setAttribute("style", "fill: #ffffff; stroke: none; fill-opacity: 0;");
        this.bgLayer.appendChild(rect);

        // background image
        this.backgroundImage = document.createElementNS(PencilNamespaces.svg,
            "svg:image");
        this.backgroundImage.setAttribute("x", "0");
        this.backgroundImage.setAttribute("y", "0");
        this.bgLayer.appendChild(this.backgroundImage);
        this.hasBackgroundImage = false;

        var rect = document.createElementNS(PencilNamespaces.svg, "svg:rect");
        rect.setAttribute("x", "0");
        rect.setAttribute("y", "0");
        rect.setAttribute("width", "2000");
        rect.setAttribute("height", "2000");
        rect.setAttributeNS(PencilNamespaces.p, "p:name", "BackgroundDimmer");
        this.bgLayer.appendChild(rect);

        this.topGroup.appendChild(this.bgLayer);

        // create the drawing layer
        this.drawingLayer = document.createElementNS(PencilNamespaces.svg, "svg:g");
        this.topGroup.appendChild(this.drawingLayer);

        // create the control layer
        this.controlLayer = document.createElementNS(PencilNamespaces.svg, "svg:g");
        this.controlLayer.setAttributeNS(PencilNamespaces.p, "p:type",
            "ControlLayer");

        this.topGroup.appendChild(this.controlLayer);

        this.selectionContainer = document.createElementNS(PencilNamespaces.svg,
            "svg:g");
        this.selectionContainer.setAttribute("id", "selectionContainer");
        this.installControlSVGElement(this.selectionContainer);

        this.rangeBoundRect = document.createElementNS(PencilNamespaces.svg,
            "svg:rect");
        this.installControlSVGElement(this.rangeBoundRect);
        Svg.setX(this.rangeBoundRect, 0.5);
        Svg.setY(this.rangeBoundRect, 0.5);
        Svg.setWidth(this.rangeBoundRect, 100);
        Svg.setHeight(this.rangeBoundRect, 100);
        this.rangeBoundRect.setAttributeNS(PencilNamespaces.p, "p:type",
            "RangeBound");

        this.snappingHelper = new SnappingHelper(this);

        this.idSeed = 1;

        this.onScreenEditors = [];

        // register event handler
        this.svg.addEventListener("click", function (event) {
            thiz.handleClick(event);
        }, false);
        (containerScrollPane || this.svg).addEventListener("mousedown", function (event) {
            thiz.movementDisabled = Pencil.controller.movementDisabled || event.ctrlKey;
            // document.commandDispatcher.advanceFocus();
            thiz.focus();
            thiz.handleMouseDown(event);
        }, false);

        this.element.addEventListener("mousewheel", function (event) {
            thiz.focus();
            thiz.handleMouseWheel(event);
        }, false);
        this.svg.ownerDocument.addEventListener("mouseup", function (event) {
            if (thiz.autoScrollTimout) {
                thiz.stopAutoScrollFunction();
            }
            if (!thiz || !thiz.handleMouseUp) {
                document.removeEventListener("mouseup", arguments.callee, false);
                return;
            }
            thiz.handleMouseUp(event);
        }, false);
        this.svg.ownerDocument.addEventListener("mousemove", function (event) {
            if (thiz.autoScrollTimout) {
                thiz.stopAutoScrollFunction();
            }
            if (!thiz || !thiz.handleMouseMove) {
                document.removeEventListener("mousemove", arguments.callee, false);
                return;
            }
            if (thiz.controllerHeld && thiz.currentController && thiz._scrollPane &&
                (thiz._scrollPane.clientHeight < thiz._scrollPane.scrollHeight || thiz._scrollPane.clientWidth < thiz._scrollPane.scrollWidth)) {
                thiz.handleScrollPane(event);
            }
            thiz.handleMouseMove(event);
        }, false);

        this.svg.addEventListener("mousemove", function (event) {
            thiz.lastMouse = {x: event.offsetX / thiz.zoom, y: event.offsetY / thiz.zoom};
        }.bind(this), false);

        this.focusableBox.addEventListener("keydown", function (event) {
            thiz.handleKeyPress(event);
        }, false);

        // this.focusableBox.addEventListener("keyup", function (event) {
        //     if (event.keyCode == DOM_VK_SHIFT) {
        //         if(thiz.duplicateMode) {
        //             thiz.duplicateMode = false;
        //             console.log(thiz.mouseUp);
        //             if (!thiz.mouseUp) {
        //                 thiz.run(function () {
        //                     thiz.currentController.deleteTarget();
        //                 }, thiz, Util.getMessage("action.delete.shape",
        //                         thiz.currentController.getName()));
        //                 thiz.currentController = null;
        //                 thiz._detachEditors();
        //                 thiz.clearSelection();
        //                 thiz._sayTargetChanged();
        //                 event.preventDefault();
        //
        //                 if (thiz.oldTargets) {
        //                     if (!thiz.oldTargets.targets) {
        //                         thiz.addToSelection(thiz.oldTargets);
        //                         thiz.currentController = thiz.oldTargets;
        //                         thiz.reClick = false;
        //                         thiz._attachEditors(thiz.currentController);
        //                     } else {
        //                         for(i in thiz.oldTargets.targets) {
        //                             thiz.addToSelection(thiz.oldTargets.targets[i]);
        //                         }
        //                         thiz.currentController = thiz.oldTargets;
        //                     }
        //                     thiz.oldTargets = null;
        //                     thiz._sayTargetChanged();
        //                 }
        //             }
        //         }
        //     }
        // }, false);

        this.svg.ownerDocument.addEventListener("keydown", function (event) {
            if (event.keyCode == DOM_VK_SPACE && thiz.spaceHeld == false) {
                thiz.spaceHeld = true;
                thiz._lastPX = thiz._currentPX;
                thiz._lastPY = thiz._currentPY;
                thiz._lastScrollX = thiz._scrollPane.scrollLeft || 0;
                thiz._lastScrollY = thiz._scrollPane.scrollTop || 0;
                Dom.addClass(thiz, "PanDown");
            }
        }, false);
        this.svg.ownerDocument.addEventListener("keyup", function (event) {
            if (event.keyCode == DOM_VK_SPACE && thiz.spaceHeld == true) {
                thiz.spaceHeld = false;
                Dom.removeClass(thiz, "PanDown");
            }

        }, false);

        this.propertyMenuItem.addEventListener("command", function (event) {
            // thiz.handlePropertyMenuItemCommand(event);
        }, false);

        this.element.addEventListener("contextmenu", function (event) {
            thiz.handleContextMenuShow(event);
        }, false);

        this.svg.addEventListener("dblclick", function (event) {
            stencilDebug("pCanvas, dblclick");
            thiz.handleDblclick(event);
        }, true);

        this.careTaker = new CanvasCareTaker(this);

        /*
         * this.dragObserver = { getSupportedFlavours : function () { var flavours =
         * new FlavourSet();
         *
         * flavours.appendFlavour("pencil/def");
         * flavours.appendFlavour("pencil/shortcut");
         * flavours.appendFlavour("text/unicode");
         * //flavours.appendFlavour("image/png");
         *
         * return flavours; }, onDragOver: function (evt, flavour, session){},
         * onDrop: function (evt, transferData, session) {
         *
         * var defId = transferData.data; var def =
         * CollectionManager.shapeDefinition.locateDefinition(defId);
         *
         * var loc = thiz.getEventLocation(evt);
         *
         * if (loc.x <0 || loc.y < 0) return;
         *
         * thiz.insertShape(def, new Bound(loc.x, loc.y, null, null)); } };
         */

        this.nVGridPainted = 0;
        this.nHGridPainted = 0;

        this.zoomTo(1);

        Pencil.installEditors(this);
        Pencil.installXferHelpers(this);
        Pencil.installDragObservers(this);
        this.careTaker.reset();

        this.snappingHelper.rebuildSnappingGuide();

        this.dragOverlay = this.element.ownerDocument.createElement("div");
        this.element.appendChild(this.dragOverlay);
        Dom.addClass(this.dragOverlay, "DragOverlay");

        this.focusableBox.addEventListener("focus", function (event) {
            Canvas.activeCanvas = thiz;
            Dom.cancelEvent(event);
        }, true);

        this.setupEventHandlers();
        window.globalEventBus.listen("config-change", function (data) {
            if (["grid.enabled", "edit.gridSize", "edit.gridStyle"].indexOf(data.name) >= 0) {
                CanvasImpl.setupGrid.apply(this);
            }
        }.bind(this));
        window.globalEventBus.listen("doc-options-change", function (data) {
            CanvasImpl.drawMargin.apply(this);
            this.snappingHelper.rebuildSnappingGuide();
        }.bind(this));

        this.resizer = this.element.ownerDocument.createElement("div");
        this.element.appendChild(this.resizer);
        Dom.addClass(this.resizer, "CanvasResizer");
        this.resizeInfoLabel = this.element.ownerDocument.createElement("span");
        this.resizer.appendChild(this.resizeInfoLabel);

        this.resizer.addEventListener("mousedown", function (event) {
            event.preventDefault();
            if (this.element.hasAttribute("resizing")) {
                this.resizing = true;
                this.resizeInfo = {
                    ox: event.clientX,
                    oy: event.clientY,
                    ow: this.width,
                    oh: this.height
                };
                this.resizeInfoLabel.innerHTML = this.width + " x " + this.height;
                return;
            }
        }.bind(this), false);
    }

    startAutoScrollFunction(func) {
        if (this.autoScrollTimout == null) {
            // this.lockPointerFunction = function () {
            //     thiz.element.requestPointerLock();
            // }
            // this.lockPointerFunction();
            this.autoScrollTimout = window.setInterval(func, 50);
        }
    }

    stopAutoScrollFunction() {
        if (this.autoScrollTimout) {
            // if (thiz.lockPointerFunction != null) {
            //     document.exitPointerLock();
            //     thiz.lockPointerFunction = null;
            // }
            clearInterval(this.autoScrollTimout);
            this.autoScrollTimout = null;
        }
    }


    createElementByName(name) {
        return this.element.ownerDocument.createElement("span");
    };

    __delegate() {
        for (var i = 0; i < arguments.length; i++) {
            this.__delegateOne(arguments[i]);
        }
    };

    __delegateOne(name) {
        var thiz = this;
        this[name] = function () {
            var f = thiz.element[name];
            var args = [];
            for (var i = 0; i < arguments.length; i++) {
                args.push(arguments[i]);
            }
            f.apply(thiz.element, args);
        };
    };

    setupEventHandlers() {
        var thiz = this;
        Dom.registerEvent(this.element, "dragenter", function (event) {
            thiz.__dragenter(event);
        }, false);
        Dom.registerEvent(this.dragOverlay, "dragleave", function (event) {
            thiz.__dragleave(event);
        }, false);
        Dom.registerEvent(this.element, "dragend", function (event) {
            thiz.__dragend(event);
        }, false);
        Dom.registerEvent(this.dragOverlay, "dragover", function (event) {
            thiz.__dragover(event);
        }, false);
        Dom.registerEvent(this.dragOverlay, "drop", function (event) {
            thiz.__drop(event);
        }, false);
    };

    getEventLocation(event, withoutZoom?): {x: number, y: number} {

        var rect = this.svg.parentNode.getBoundingClientRect();
        var x = Math.round(event.clientX - rect.left);
        var y = Math.round(event.clientY - rect.top);

        if (withoutZoom) {
            x /= this.zoom;
            y /= this.zoom;
        }

        return {
            x: x,
            y: y
        };

    };

    addToSelection(target) {

        var rect = document.createElementNS(PencilNamespaces.svg, "svg:rect");
        this.selectionContainer.appendChild(rect);
        rect._target = target;

        this._invalidateOneSelection(rect);

    };

    _invalidateOneSelection(rect) {

        var bbox = rect._target.getBoundingRect();
        Svg.setX(rect, Math.round(bbox.x));
        Svg.setY(rect, Math.round(bbox.y));
        Svg.setWidth(rect, Math.round(bbox.width));
        Svg.setHeight(rect, Math.round(bbox.height));

    };

    _sayTargetChanged() {

        Dom.emitEvent("p:TargetChanged", this.element, {
            canvas: this
        });

    };

    getSelectedTargets() {

        var targets = [];
        for (var i = 0; i < this.selectionContainer.childNodes.length; i++) {
            var rect = this.selectionContainer.childNodes[i];
            if (rect._target)
                targets.push(rect._target);
        }

        return targets;

    };

    clearSelection() {
        while (this.selectionContainer.hasChildNodes()) {
            var child = this.selectionContainer.firstChild;
            this.selectionContainer.removeChild(child);
        }

    };

    selectSibling(next) {

        var targets = this.getSelectedTargets();
        var node = null;

        var sibling = null;

        if (targets && targets[0] && targets[0].svg) {
            node = targets[0].svg;

            if (next) {
                if (node.nextSibling) {
                    sibling = node.nextSibling;
                } else {
                    sibling = this.drawingLayer.firstChild;
                }
            } else {
                if (node.previousSibling) {
                    sibling = node.previousSibling;
                } else {
                    sibling = this.drawingLayer.lastChild;
                }
            }

        } else {
            sibling = this.drawingLayer.firstChild;
        }

        if (!sibling)
            return;

        this.selectShape(sibling);

    };

    invalidateAll(callback) {
        if (this.element.clientWidth <= 0) {
            setTimeout(function () {
                this.invalidateAll(callback);
            }.bind(this), 10);
            return;
        }

        try {
            Dom.workOn(".//svg:g[@p:type='Shape']", this.drawingLayer, function (node) {
                try {
                    var controller = this.createControllerFor(node);
                    if (controller && controller.validateAll) controller.validateAll();
                } catch (e) {
                    console.error(e);
                }
            }.bind(this));
        } finally {
            if (callback) callback();
        }
    };

    selectAll() {

        this.clearSelection();
        var thiz = this;
        Dom.workOn("./svg:g[@p:type]", this.drawingLayer, function (node) {
            if (thiz.isShapeLocked(node))
                return;
            try {
                var controller = thiz.createControllerFor(node);
                thiz.addToSelection(controller);
            } catch (e) {
                // alert(e);
            }
        });

        var targets = this.getSelectedTargets();
        this.setAttributeNS(PencilNamespaces.p, "p:selection", targets.length);

        var controller = null;
        if (targets.length > 1) {
            controller = new TargetSet(this, targets);
        } else if (targets.length == 1 && controller == null) {
            controller = targets[0];
        }

        this._detachEditors();
        this.currentController = controller;

        this._attachEditors(this.currentController);
        this._sayTargetChanged();

    };

    removeFromSelection(target) {

        var foundRect = null;
        for (var i = 0; i < this.selectionContainer.childNodes.length; i++) {
            var rect = this.selectionContainer.childNodes[i];
            if (rect._target == target) {
                foundRect = rect;
                break;
            }
        }
        if (foundRect)
            this.selectionContainer.removeChild(foundRect);

    };

    insertObject(obj) {

        this.drawingLayer.appendChild(obj);

    };

    installControlSVGElement(obj) {

        this.controlLayer.appendChild(obj);

    };

    zoomTo(factor) {

        this.zoom = factor;
        this.drawingLayer.setAttribute("transform", "scale("
            + [this.zoom, this.zoom] + ")");
        this.svg.setAttribute("width", Math.ceil(this.width * this.zoom));
        this.svg.setAttribute("height", Math.ceil(this.height * this.zoom));
        this.backgroundImage.setAttribute("transform", "scale("
            + [this.zoom, this.zoom] + ")");
        CanvasImpl.setupGrid.apply(this);

        this.invalidateEditors();

        Dom.emitEvent("p:SizeChanged", this.element, {
            canvas: this
        });
        Dom.emitEvent("p:ZoomChanged", this.element, {
            canvas: this
        });

    };

    getZoomedGeo(target) {

        if (!target)
            return null;
        var geo = target.getGeometry();
        geo = geo.clone(this.svg);
        geo.ctm.e *= this.zoom;
        geo.ctm.f *= this.zoom;
        geo.dim.w *= this.zoom;
        geo.dim.h *= this.zoom;

        if (geo.loc) {
            geo.loc.x *= this.zoom;
            geo.loc.y *= this.zoom;
        }

        return geo;

    };

    getSize() {

        return {
            width: parseInt(this.svg.getAttribute("width"), 10),
            height: parseInt(this.svg.getAttribute("height"), 10),
        };

    };

    getZoomedRect(rect: {x: number, y: number, width: number, height: number}) {

        rect.x *= this.zoom;
        rect.y *= this.zoom;
        rect.width *= this.zoom;
        rect.height *= this.zoom;

        return rect;

    };

    setZoomedGeo(target, geo, setter) {

        geo = geo.clone(this.svg);
        geo.ctm.e /= this.zoom;
        geo.ctm.f /= this.zoom;
        geo.dim.w /= this.zoom;
        geo.dim.h /= this.zoom;

        this.run(function () {
            target.setGeometry(geo);
        }, this, Util.getMessage("action.canvas.zoom"));

        Dom.emitEvent("p:ShapeGeometryModified", this.element, {
            setter: setter ? setter : null
        });

    };

    redraw() {

        this.drawingLayer.ownerSVGElement.forceRedraw();

    };

    getType(svg) {

        return svg.getAttributeNS(PencilNamespaces.p, "def");

    };

    insertShape(shapeDef, bound, overridingValueMap) {

        this.run(this.insertShapeImpl_, this, Util.getMessage(
            "action.create.shape", shapeDef.displayName), [shapeDef,
            bound ? bound : null,
            overridingValueMap ? overridingValueMap : null]);

    };

    invalidateShapeContent(shape, shapeDef) {
        var count = shape.childNodes.length;
        for (var i = count - 1; i >= 0; i--) {
            var child = shape.childNodes[i];
            if (child.namespaceURI == PencilNamespaces.p && child.localName == "metadata") continue;
            shape.removeChild(child);
        }
        for (var i = 0; i < shapeDef.contentNode.childNodes.length; i++) {
            shape.appendChild(this.ownerDocument.importNode(
                shapeDef.contentNode.childNodes[i], true));
        }

        // generate the ids
        Dom.workOn(".//*[@p:name]", shape, function (node) {
            var name = node.getAttributeNS(PencilNamespaces.p, "name");
            var oldId = node.getAttribute("id");
            if (oldId)
                return;

            var uuid = Util.newUUID();
            node.setAttribute("id", uuid);
            node.id = uuid;

            Dom.updateIdRef(shape, name, uuid);
        });

        Dom.renewId(shape);
    };

    insertShapeImpl_(shapeDef, bound,
                     overridingValueMap) {

        // instantiate the shape using the shapedef
        var shape = this.ownerDocument.createElementNS(PencilNamespaces.svg, "g");
        shape.setAttributeNS(PencilNamespaces.p, "p:type", "Shape");
        shape.setAttributeNS(PencilNamespaces.p, "p:def", shapeDef.id);


        if (overridingValueMap && overridingValueMap._shortcut) {
            shape.setAttributeNS(PencilNamespaces.p, "p:sc",
                overridingValueMap._shortcut.displayName);
        }

        shape.appendChild(this.ownerDocument.createElementNS(PencilNamespaces.p,
            "p:metadata"));

        this.invalidateShapeContent(shape, shapeDef);

        // add the newly created shape into the drawing layer
        this.drawingLayer.appendChild(shape);

        // applying defined behaviors into the shape

        var controller = new Shape(this, shape);
        controller.setInitialPropertyValues(overridingValueMap);

        if (bound) {
            var bbox = controller.getBoundingRect();
            controller.moveBy((bound.x - Math.round(bbox.width / (2 * this.zoom))),
                (bound.y - Math.round(bbox.height / (2 * this.zoom))), true);
            controller.normalizePositionToGrid();
        }
        this.selectShape(shape);
        this.snappingHelper.updateSnappingGuide(this.currentController);
        DockingManager.enableDocking(this.currentController);

    };

    selectShape(shape: SVGElement): void {

        if (this.isShapeLocked(shape)) return;
        var controller = this.createControllerFor(shape);

        this.currentController = controller;

        this.clearSelection();

        this.addToSelection(this.currentController);
        this.setAttributeNS(PencilNamespaces.p, "p:selection", 1);

        this.lastTop = shape;
        this.hasMoved = true;

        this.focus();

        this._attachEditors(controller);

        this._sayTargetChanged();

    };

    selectMultiple(shapes) {

        this.clearSelection();
        for (let i in shapes) {
            if (this.isShapeLocked(shapes[i]))
                continue;
            this.addToSelection(this.createControllerFor(shapes[i]));
        }
        this.setAttributeNS(PencilNamespaces.p, "p:selection", shapes.length);

        var targets = this.getSelectedTargets();
        let controller;
        if (targets.length > 1) {
            controller = new TargetSet(this, targets);
        } else if (targets.length == 1 && controller == null) {
            controller = targets[0];
        }

        this.currentController = controller;

        this.lastTop = null;
        this.hasMoved = true;

        this.focus();

        this._attachEditors(controller);

        this._sayTargetChanged();

    };

    selectNone(): void {

        this.clearSelection();
        this._detachEditors();
        this.currentController = null;

        this.lastTop = null;
        this.hasMoved = true;

        this.focus();
        this._sayTargetChanged();

    };

    createControllerFor(top) {

        var type = top.getAttributeNS(PencilNamespaces.p, "type");
        if (type == "Shape") {
            try {
                return new Shape(this, top);
            } catch (e) {
            }
        } else if (type == "Group") {
            return new Group(this, top);
        }
        return new Null(this, top);

    };

    _detachEditors() {
        for (var editor in this.onScreenEditors)
            this.onScreenEditors[editor].dettach();
        if (this.propertyPageEditor && this.propertyPageEditor.dettach)
            this.propertyPageEditor.dettach();
        if (this.contextMenuEditor && this.contextMenuEditor.dettach)
            this.contextMenuEditor.dettach();

    };

    passivateEditors() {

        for (editor in this.onScreenEditors)
            this.onScreenEditors[editor].passivated = true;
        if (this.propertyPageEditor)
            this.propertyPageEditor.passivated = true;
        if (this.contextMenuEditor)
            this.contextMenuEditor.passivated = true;

    };

    _attachEditors(controller) {
        for (let editor in this.onScreenEditors)
            this.onScreenEditors[editor].attach(controller);
        if (this.propertyPageEditor && this.propertyPageEditor.attach)
            this.propertyPageEditor.attach(controller);
        // if (this.contextMenuEditor && this.contextMenuEditor.attach)
        //     this.contextMenuEditor.attach(controller);

    };

    finishMoving(event) {

        if (this.controllerHeld && this.currentController
            && this.currentController.markAsMoving) {
            this.currentController.markAsMoving(false);
            this.currentController.clearPositionSnapshot();
            this.invalidateEditors();

            this.snappingHelper.updateSnappingGuide(this.currentController);
            this.snappingHelper.clearSnappingGuide();

            Dom.emitEvent("p:ShapeGeometryModified", this.element, {
                setter: null
            });

            if (this.hasMoved) {
                // just to save state
                this.run(function () {
                }, this, Util.getMessage("action.shape.finish.moving",
                    this.currentController.getName()));
            }
        }

        this.removeAttributeNS(PencilNamespaces.p, "holding");
        this.controllerHeld = false;

        if (Config.get("quick.editting", false) == true) {
            Dom.emitEvent("p:ShapeInserted", this.currentController.svg, {
                controller: this.currentController,
                origTarget: event.originalTarget,
                clientX: event.clientX,
                clientY: event.clientY
            });
        }
    };

    handleMouseWheel(event) {
        if (event.ctrlKey) {
            Dom.cancelEvent(event);

            const padding = 0;

            var drawingX = this.lastMouse.x;
            var drawingY = this.lastMouse.y;
            var dx = drawingX * this.zoom + padding - this._scrollPane.scrollLeft;
            var dy = drawingY * this.zoom + padding - this._scrollPane.scrollTop;

            if (event.deltaY < 0) {
                this.zoomTo(this.zoom * 1.25);
            } else {
                this.zoomTo(this.zoom / 1.25);
            }

            this._scrollPane.scrollLeft = drawingX * this.zoom + padding - dx;
            this._scrollPane.scrollTop = drawingY * this.zoom + padding - dy;
        }
    }

    handleScrollPane(event) {
        if (!this._scrollPane) return;
        var thiz = this;
        var scrollBarSize = 15;
        var scrollValue = 20;
        var loc = {x: event.clientX, y: event.clientY};
        var pane = thiz._scrollPane.getBoundingClientRect();
        var fun = null;
        var dx = scrollValue / thiz.zoom;
        var dy = scrollValue / thiz.zoom;
        if (loc.x > pane.right - scrollBarSize && loc.x < pane.right) {
            fun = function () {
                if (thiz._scrollPane.scrollLeft >= thiz._scrollPane.scrollWidth - thiz._scrollPane.offsetWidth) {
                    thiz._scrollPane.scrollLeft = thiz._scrollPane.scrollWidth;
                    thiz.stopAutoScrollFunction();
                    return;
                }
                thiz._scrollPane.scrollLeft += scrollValue;
                if (thiz.currentController != null) thiz.currentController.moveBy(dx, 0, false, true);
            }
        }
        if (loc.x < pane.left && loc.x > pane.left - scrollBarSize) {
            fun = function () {
                if (thiz._scrollPane.scrollLeft <= 0) {
                    thiz.stopAutoScrollFunction();
                    return;
                }
                thiz._scrollPane.scrollLeft -= scrollValue;
                if (thiz.currentController != null) thiz.currentController.moveBy(-dx, 0, false, true);
            }
        }
        if (loc.y < pane.top && loc.y > pane.top - scrollBarSize) {
            fun = function () {
                if (thiz._scrollPane.scrollTop <= 0) {
                    thiz.stopAutoScrollFunction();
                    return;
                }
                thiz._scrollPane.scrollTop -= scrollValue;
                if (thiz.currentController != null) thiz.currentController.moveBy(0, -dy, false, true);
            }
        }
        if (loc.y > pane.bottom - scrollBarSize && loc.y < pane.bottom) {
            fun = function () {
                if (thiz._scrollPane.scrollTop >= thiz._scrollPane.scrollHeight - thiz._scrollPane.offsetHeight) {
                    thiz._scrollPane.scrollTop = thiz._scrollPane.scrollHeight;
                    thiz.stopAutoScrollFunction();
                    return;
                }
                thiz._scrollPane.scrollTop += scrollValue;
                if (thiz.currentController != null) thiz.currentController.moveBy(0, dy, false, true);
            }
        }
        if (fun != null) {
            thiz.startAutoScrollFunction(fun);
        }
    }

    handleMouseUp(event) {
        if (this.resizing) {
            this.commitResize(event);
            this.isSelectingRange = false;
            return;
        }

        if (this.reClick && !this.hasMoved) {
            for (editor in this.onScreenEditors)
                this.onScreenEditors[editor].nextTool();
        }
        if (this.controllerHeld && this.currentController
            && this.currentController.markAsMoving) {
            this.currentController.markAsMoving(false);
            this.currentController.clearPositionSnapshot();
            this.invalidateEditors();

            if (this.hasMoved) {
                Dom.emitEvent("p:ShapeGeometryModified", this.element, {
                    setter: null
                });
            }

            Connector.prepareInvalidation(this);

            if (this.currentController.invalidateOutboundConnections) {
                this.currentController.invalidateOutboundConnections();
            }
            if (this.currentController.invalidateInboundConnections) {
                this.currentController.invalidateInboundConnections();
            }

            Connector.finishInvalidation();
        }
        if (this.controllerHeld && this.hasMoved) {
            // just to save state
            this.run(function () {
            }, this, Util.getMessage("action.move.shape"));
        }

        this.removeAttributeNS(PencilNamespaces.p, "holding");

        this.reClick = false;
        this.hasMoved = true;

        this.controllerHeld = false;

        if (this.isSelectingRange) {
            this.setRangeBoundVisibility(false);
            this.isSelectingRange = false;
            // enum objects that are in range
            if (!this.isEventWithControl(event)) {
                this.clearSelection();
            }
            var thiz = this;
            Dom.workOn("./svg:g[@p:type]", this.drawingLayer, function (node) {
                if (thiz.isShapeLocked(node))
                    return;
                var controller = thiz.createControllerFor(node);
                var bbox = controller.getBoundingRect();

                if (Svg.isInside(bbox, thiz.currentRange)) {
                    if (thiz.isEventWithControl(event)) {
                        var targets = thiz.getSelectedTargets();
                        var foundTarget = null;
                        for (i in targets) {
                            var target = targets[i];
                            if (target.isFor(node)) {
                                foundTarget = target;
                                break;
                            }
                        }

                        if (foundTarget) {
                            thiz.removeFromSelection(foundTarget);
                        } else {
                            var target = thiz.createControllerFor(node);
                            thiz.addToSelection(target);
                        }
                    } else {
                        var target = thiz.createControllerFor(node);
                        thiz.addToSelection(target);
                    }
                }
            });
            var controller = null;
            var targets = this.getSelectedTargets();
            this.setAttributeNS(PencilNamespaces.p, "p:selection", targets.length);

            if (targets.length > 1) {
                controller = new TargetSet(this, targets);
            } else if (targets.length == 1 && controller == null) {
                controller = targets[0];
            }

            this._detachEditors();
            if (controller) {
                this.currentController = controller;
                this._attachEditors(this.currentController);
            }
            this.currentRange = null;
            this._sayTargetChanged();
        }

        try {
            this.snappingHelper.clearSnappingGuide();

            if (this.currentController) {
                if (this.currentController.constructor == TargetSet) {
                    for (var t in this.currentController.targets) {
                        this.snappingHelper
                            .updateSnappingGuide(this.currentController.targets[t]);
                    }
                } else {
                    this.snappingHelper.updateSnappingGuide(this.currentController);
                }
            }

            DockingManager.enableDocking(this.currentController);

            // var canvas = Dom.findUpward(event.originalTarget, function (node) {
            //     return node.namespaceURI == PencilNamespaces.xul
            //             && node.localName == "pcanvas";
            // });

            var canvasElement = Dom.findTop(event.originalTarget, function (node) {
                return node.getAttribute && node.getAttribute("pencil-canvas") == "true";
            });

            if (canvasElement && canvasElement == this.element && this.currentController) {
                if (this.isFormatPainterAvailable()
                    && Pencil._painterSourceTarget.svg != this.currentController.svg) {
                    var currentTargetProperties = this.currentController
                        .getProperties();
                    for (var i = 0; i < this.painterPropertyMap.length; i++) {
                        var name = this.painterPropertyMap[i];
                        var p1 = Pencil._painterSourceProperties[name];
                        var p2 = currentTargetProperties[name];
                        if (p1 && p2) {
                            this.currentController.setProperty(name,
                                Pencil._painterSourceProperties[name]);
                            this.invalidateEditors();
                            this._sayTargetChanged();
                        }
                    }
                    return;
                }
                this.endFormatPainter();
            }
        } catch (e) {
            Console.dumpError(e);
        }

    };

    handleClick(event) {

        // is it from an html:a?
        var a = Dom.findUpward(
                event.originalTarget,
                function (node) {
                    return (node && node.nodeType == 1
                        && node.localName.toLowerCase() == "a" && node.namespaceURI == PencilNamespaces.html);
                });

        if (!a)
            return;

        // is this html:a inside an svg:foreignObject?
        var foreignObject = Dom
            .findUpward(
                a,
                function (node) {
                    return (node && node.nodeType == 1
                        && node.localName == "foreignObject" && node.namespaceURI == PencilNamespaces.svg);
                });

        // ok, it is then prevent it
        if (foreignObject) {
            event.preventDefault();
            event.cancelBubble = true;
        }

    };

    commitResize(event) {
        this.resizing = false;
        if (this.resizeInfo && this.resizeInfo.lastSize) {
            Pencil.controller.setActiveCanvasSize(this.resizeInfo.lastSize.w, this.resizeInfo.lastSize.h)
        }
        this.resizeInfo = null;
        this.element.removeAttribute("resizing");
    };

    handleResizeMouseMove(event) {
        if (this.resizing) {

            var dw = Math.round((event.clientX - this.resizeInfo.ox) / this.zoom);
            var dh = Math.round((event.clientY - this.resizeInfo.oy) / this.zoom);

            if (event.shiftKey) dw = 0;

            var newW = this.resizeInfo.ow + dw;
            var newH = this.resizeInfo.oh + dh;

            if (event.ctrlKey) {
                newW = Math.round(newW / 10) * 10;
                newH = Math.round(newH / 10) * 10;
            }

            this.resizeInfo.lastSize = {
                w: newW,
                h: newH
            };

            var w = Math.ceil(newW * this.zoom);
            var h = Math.ceil(newH * this.zoom);
            this.element.style.width = w + "px";
            this.element.style.height = h + "px";

            this.resizeInfoLabel.innerHTML = w + " x " + h;

            return;
        }

        var rect = this.svg.parentNode.getBoundingClientRect();
        var bound = {
            x: rect.left + rect.width - this.resizer.offsetWidth,
            y: rect.top + rect.height - this.resizer.offsetHeight,
            width: this.resizer.offsetWidth,
            height: this.resizer.offsetHeight
        };

        var thiz = this;
        if (event.clientX >= bound.x && event.clientX <= bound.x + bound.width
            && event.clientY >= bound.y && event.clientY <= bound.y + bound.width) {
            if (!this.showResizerTimeout) {
                this.showResizerTimeout = window.setTimeout(function () {
                    thiz.showResizerTimeout = null;
                    thiz.element.setAttribute("resizing", "true");
                    thiz.resizeInfoLabel.innerHTML = thiz.width + " x " + thiz.height;
                }, 1000);
            }
        } else {
            if (this.showResizerTimeout) window.clearTimeout(this.showResizerTimeout);
            this.element.removeAttribute("resizing");
            this.showResizerTimeout = null;
        }
        return false;
    };

    handleMouseMove(event, fake) {
        if (!fake && this.handleResizeMouseMove(event)) return;

        try {
            if (this.duplicateMode && !this.mouseUp) {
                if (this.duplicateFunc) {
                    this.duplicateFunc();
                }
            }

            this._currentPX = event.clientX / this.zoom;
            this._currentPY = event.clientY / this.zoom;

            if (this.spaceHeld) {
                var spanx = this._lastScrollX
                    + (this._lastPX - Math.round(this._currentPX)) * this.zoom;
                var spany = this._lastScrollY
                    + (this._lastPY - Math.round(this._currentPY)) * this.zoom;

                this.parentNode.scrollTop = spany;
                this.parentNode.scrollLeft = spanx;

                return;
            }

            if (event.originalTarget
                && event.originalTarget.nodeName
                && "menu,menuitem,menuseparator,toolbar"
                    .indexOf(event.originalTarget.nodeName) == -1) {
                var rect = this.svg.parentNode.getBoundingClientRect();
                var boxObject = {
                    x: rect.left,
                    y: rect.top,
                    width: rect.width,
                    height: rect.height
                };
                var px = this._currentPX - boxObject.x;
                var py = this._currentPY - boxObject.y;
                // shadow 5
                if (px >= 0 && px <= boxObject.width - 5 && py >= 0
                    && py <= boxObject.height - 5) {
                    // setTimeout(function () { Util.setPointerPosition(px, py); },
                    // 100);
                }
            }

            if (this._button != 0)
                return;

            if (this.controllerHeld && this.currentController) {
                if (!fake) {
                    event.preventDefault();
                    event.stopPropagation();

                    if (this.movementDisabled) return;
                }

                //avoid accidental move when user is trying to select the object
                var msFromClick = event.timeStamp - this._mouseDownAt;
                if (msFromClick < 100) {
                    return;
                }

                if (this.currentController.markAsMoving)
                    this.currentController.markAsMoving(true);
                var newX = Math.round(event.clientX / this.zoom);
                var newY = Math.round(event.clientY / this.zoom);

                var dx = newX - this.oX;
                var dy = newY - this.oY;

                //direction ratios
                var hdr = event.ctrlKey && Math.abs(dx) < Math.abs(dy) ? 0 : 1;
                var vdr = event.ctrlKey && Math.abs(dx) >= Math.abs(dy) ? 0 : 1;

                var accX = Math.abs(newX - this._lastNewX) < 2;
                var accY = Math.abs(newY - this._lastNewY) < 2;

                this._lastNewX = newX;
                this._lastNewY = newY;

                this.currentX = newX;
                this.currentY = newY;


                // this.oX = newX;
                // this.oY = newY;

                this.hasMoved = true;

                var gridSize = Pencil.getGridSize();
                var snap = null;
                if (Config.get("object.snapping.enabled", true) == true) {
                    snap = this.snappingHelper.findSnapping(accX
                        && !this.snappingHelper.snappedX, accY
                        && !this.snappingHelper.snappedY, null, null,
                        event.shiftKey);
                }
                if (Config.get("edit.snap.grid", false) == true) {
                    var snapGrid = this.snappingHelper.findSnapping(accX
                        && !this.snappingHelper.snappedX, accY
                        && !this.snappingHelper.snappedY, null, gridSize.w / 2,
                        event.shiftKey, true);
                    if (snap && snapGrid) {
                        if (snap.dx == 0) {
                            snap.dx = snapGrid.dx;
                        }
                        if (snap.dy == 0) {
                            snap.dy = snapGrid.dy;
                        }
                    } else {
                        snap = snapGrid;
                    }
                    // debug("snap grid: " + [snapGrid.dx, snapGrid.dy]);
                }
                // debug("snap: " + [snap.dx, snap.dy, this.snappedX,
                // this.snappedY]);
                if (!event.shiftKey
                    && snap
                    && ((snap.dx != 0 && !this.snappingHelper.snappedX && accX)
                        || (snap.dy != 0 && !this.snappingHelper.snappedY && accY)
                    )) {
                    if (snap.dx != 0 && !this.snappingHelper.snappedX) {
                        this.snappingHelper.snappedX = true;
                        this.snappingHelper.snapX = newX;
                        this.currentController._pSnapshot.lastDX += snap.dx;
                        // debug("snapX");
                    }
                    if (snap.dy != 0 && !this.snappingHelper.snappedY) {
                        this.snappingHelper.snappedY = true;
                        this.snappingHelper.snapY = newY;
                        this.currentController._pSnapshot.lastDY += snap.dy;
                        // debug("snapY");
                    }
                    this.currentController.moveBy(snap.dx * hdr, snap.dy * vdr);
                } else {
                    var unsnapX = event.shiftKey
                        || (this.snappingHelper.snapX != 0 && (Math
                            .abs(this.snappingHelper.snapX - newX) > this.snappingHelper.unsnapX));
                    var unsnapY = event.shiftKey
                        || (this.snappingHelper.snapY != 0 && (Math
                            .abs(this.snappingHelper.snapY - newY) > this.snappingHelper.unsnapY));
                    // debug("unsnap: " + [unsnapX, unsnapY]);

                    if (!this.snappingHelper.snappedX
                        && !this.snappingHelper.snappedY) {
                        this.currentController.moveFromSnapshot(dx * hdr, dy * vdr);
                    } else {
                        if (unsnapX || !this.snappingHelper.snappedX) {
                            this.currentController
                                .moveFromSnapshot(
                                    dx * hdr,
                                    this.snappingHelper.snappedY ? this.currentController._pSnapshot.lastDY * vdr
                                        : dy * vdr);
                        }
                        if (unsnapY || !this.snappingHelper.snappedY) {
                            this.currentController
                                .moveFromSnapshot(
                                    this.snappingHelper.snappedX ? this.currentController._pSnapshot.lastDX * hdr
                                        : dx * hdr, dy * vdr);
                            this.snappingHelper.snapY = 0;
                            this.snappingHelper.snappedY = false;
                        }
                        if (unsnapX || !this.snappingHelper.snappedX) {
                            this.snappingHelper.snapX = 0;
                            this.snappingHelper.snappedX = false;
                        }

                        if (unsnapX) {
                            this.snappingHelper.clearSnappingGuideX();
                        }
                        if (unsnapY) {
                            this.snappingHelper.clearSnappingGuideY();
                        }
                    }
                }
                if (this.currentController.dockingManager) {
                    this.currentController.dockingManager.altKey = event.altKey;
                }
                return;
            }

            if (this.isSelectingRange) {
                var end = this.getEventLocation(event);
                var x1 = Math.min(end.x, this.lastMousePos.x);
                var x2 = Math.max(end.x, this.lastMousePos.x);
                var y1 = Math.min(end.y, this.lastMousePos.y);
                var y2 = Math.max(end.y, this.lastMousePos.y);

                var w = x2 - x1;
                var h = y2 - y1;

                this.currentRange = {
                    x: x1,
                    y: y1,
                    width: w,
                    height: h
                };
                this.setRangeBoundStart(x1, y1);
                this.setRangeBoundSize(w, h);
                this.handleScrollPane(event);
            }
        } catch (ex) {
            error(ex);
        }

    };

    setRangeBoundStart(x, y) {

        Svg.setX(this.rangeBoundRect, x + 0.5);
        Svg.setY(this.rangeBoundRect, y + 0.5);

    };

    setRangeBoundSize(w, h) {

        Svg.setWidth(this.rangeBoundRect, w);
        Svg.setHeight(this.rangeBoundRect, h);

    };

    setRangeBoundVisibility(visible) {

        this.rangeBoundRect.setAttributeNS(PencilNamespaces.p, "p:on",
            visible ? "true" : "false");

    };

    handleKeyPress(event) {
        if (this != Pencil.activeCanvas) return;

        for (editor in this.onScreenEditors) {
            var e = this.onScreenEditors[editor];
            if (e.handleKeyPressEvent) {
                if (e.handleKeyPressEvent(event)) {
                    return;
                }
            }
        }
        if (event.keyCode == DOM_VK_UP || event.keyCode == DOM_VK_DOWN
            || event.keyCode == DOM_VK_LEFT
            || event.keyCode == DOM_VK_RIGHT) {
            event.preventDefault();
            if (!this.currentController)
                return;

            var dx = 0;
            var dy = 0;

            if (event.keyCode == DOM_VK_UP)
                dy = -1;
            if (event.keyCode == DOM_VK_DOWN)
                dy = 1;
            if (event.keyCode == DOM_VK_LEFT)
                dx = -1;
            if (event.keyCode == DOM_VK_RIGHT)
                dx = 1;

            var gridSize = Pencil.getGridSize();
            if (event.shiftKey) {
                dx *= gridSize.w * 4;
                dy *= gridSize.h * 4;
            } else if (event.ctrlKey) {
                dx *= gridSize.w;
                dy *= gridSize.h;
            }

            this.run(function () {
                // this.currentController.moveBy(dx, dy);
                this.currentController.moveBy(dx, dy, false, true);

                Connector.prepareInvalidation(this);
                if (this.currentController.invalidateOutboundConnections) {
                    this.currentController.invalidateOutboundConnections();
                }
                if (this.currentController.invalidateInboundConnections) {
                    this.currentController.invalidateInboundConnections();
                }
                Connector.finishInvalidation();

            }, this, Util.getMessage("action.move.shape"), null);

            if (this.currentController.constructor == TargetSet) {
                for (var t in this.currentController.targets) {
                    this.snappingHelper
                        .updateSnappingGuide(this.currentController.targets[t]);
                }
            } else {
                this.snappingHelper.updateSnappingGuide(this.currentController);
            }

            this.invalidateEditors();
            event.preventDefault();
            Dom.emitEvent("p:ShapeGeometryModified", this.element, {
                setter: null
            });

        } else if (event.keyCode == DOM_VK_DELETE) {
            if (this.currentController) {
                this.run(function () {
                    this.currentController.deleteTarget();
                }, this, Util.getMessage("action.delete.shape",
                    this.currentController.getName()), null);
                this.currentController = null;
                this._detachEditors();
                this.clearSelection();
                this._sayTargetChanged();
                event.preventDefault();
            }
        } else if (event.keyCode == DOM_VK_F2) {
            if (this.currentController && !event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey) {
                Dom.emitEvent("p:TextEditingRequested", this.element, {
                    controller: this.currentController
                });
            }
        } else if (event.keyCode == DOM_VK_TAB) {
            if (event.shiftKey) {
                this.selectSibling(false);
            } else {
                this.selectSibling(true);
            }

            event.preventDefault();
        } else if (event.charCode == " ".charCodeAt(0) && event.ctrlKey) {
            event.preventDefault();
            this._showPropertyDialog();
            event.preventDefault();
        } else if (event.charCode == "a".charCodeAt(0) && event.ctrlKey) {
            event.preventDefault();
            this.selectAll();
            event.preventDefault();
        } else if (event.keyCode == DOM_VK_ESCAPE) {
            this.endFormatPainter();
        }
        // else if (event.keyCode == DOM_VK_SHIFT) {
        //     var targets = this.getSelectedTargets();
        //     if (targets.length > 0) {
        //         this.duplicateMode = true;
        //         this.mouseUp = false;
        //     }
        // }

    };

    updateContextMenu(currentAction, prevAction) {

        this.undoContextMenu.setAttribute("label", Util
                .getMessage("menu.undo.label")
            + currentAction);
        this.redoContextMenu.setAttribute("label", Util
                .getMessage("menu.redo.label")
            + prevAction);

    };

    handleContextMenuShow(event) {
        if (this.currentController) {
            // attach now
            if (this.contextMenuEditor) {
                this.contextMenuEditor.attach(this.currentController);
            }

            this.lockingStatus = {
                controller: this.currentController
            };

        } else {
            var top = Dom.findTop(event.originalTarget, function (node) {
                return node.hasAttributeNS
                    && node.hasAttributeNS(PencilNamespaces.p, "type");
            });
            this.currentController = top;
            if (top && this.isShapeLocked(top)) {
                this.lockingStatus = {
                    node: top
                };
            }

            if (this.contextMenuEditor) {
                this.contextMenuEditor.dettach();
            }
        }

        ApplicationPane._instance.canvasMenu.showMenuAt(event.clientX, event.clientY);
    };

    /*
        try {
            this._lastEvent = event;
            Dom.workOn("./xul:menuseparator", this.popup, function (sep) {
                sep.style.display = "";
            });
            for (var i = 0; i < this.popup.childNodes.length; i++) {
                var child = this.popup.childNodes[i];
                var forAtt = child.getAttributeNS(PencilNamespaces.p, "for");
                child._canvas = this;
                if (forAtt) {
                    forAtt = "," + forAtt + ",";
                    var visible = false;
                    if (this.currentController) {
                        if (forAtt.indexOf(","
                                + this.currentController.constructor.name + ",") >= 0) {
                            visible = true;
                        }
                    }

                    child.style.display = visible ? "" : "none";
                    child._controller = this.currentController;
                }
            }

            this.lockingMenuItem.style.display = "none";
            this.lockingStatus = null;

            if (this.currentController) {
                this.lockingStatus = {
                    controller : this.currentController
                };
                this.lockingMenuItem.style.display = "";
                this.lockingMenuItem.setAttribute("checked", "false");
            } else {
                var top = Dom.findTop(event.originalTarget, function (node) {
                    return node.hasAttributeNS
                            && node.hasAttributeNS(PencilNamespaces.p, "type");
                });
                if (top && this.isShapeLocked(top)) {
                    this.lockingStatus = {
                        node : top
                    };
                    this.lockingMenuItem.style.display = "";
                    this.lockingMenuItem.setAttribute("checked", "true");
                }
            }

            // remove all menu items previously created by the on-menu editor
            var parent = this.popupSeparator.parentNode;
            var len = parent.childNodes.length;
            for (var i = len - 1; i >= 0; i--) {
                var child = parent.childNodes[i];
                if (child._isEditor)
                    parent.removeChild(child);
            }

            if (this.currentController) {
                // attach now
                if (this.contextMenuEditor) {
                    this.contextMenuEditor.attach(this.currentController);
                }
            }

            // this.buildAttachMenuItem();
            // this.buildDetachMenuItem();

            var childs = this.popup.childNodes;
            var shouldHideNextSeparator = true;
            var foundVisibleItem = false;

            for (var i = 0; i < childs.length; i++) {
                var child = childs[i];
                if (child.localName == "menuseparator") {
                    if (shouldHideNextSeparator) {
                        child.style.display = "none";
                    } else {
                        shouldHideNextSeparator = true;
                    }
                } else {
                    if (child.style.display != "none") {
                        shouldHideNextSeparator = false;
                    }
                }

                if (child.style.display != "none") {
                    foundVisibleItem = true;
                }
            }

            for (var i = childs.length - 1; i >= 0; i--) {
                var child = childs[i];
                if (child.localName != "menuseparator"
                        && child.style.display != "none") {
                    break;
                }
                child.style.display = "none";
            }
            if (!foundVisibleItem) {
                this.popup.hidePopup();
                return;
            }
        } catch (e) {
            Console.dumpError(e);
        }

    */

    buildAttachMenuItem() {

        if (this.attachToMenu) {
            this.popup.removeChild(this.attachToMenu);
            this.attachToMenu = null;
        }
        if (!this.currentController)
            return;
        if (this.currentController.supportAttach
            && this.currentController.supportAttach()) {
            var thiz = this;
            var container = this.currentController.getAttachContainer();
            this.attachToMenu = document.createElementNS(PencilNamespaces.xul,
                "menu");
            this.attachToMenu.setAttribute("label", Util
                .getMessage("canvas.merge.label"));
            var attachSlots = this.currentController.getAttachSlots();
            if (attachSlots.length == 1) {
                this.attachToMenu = document.createElementNS(PencilNamespaces.xul,
                    "menuitem");
            }
            var menu = this.popup.insertBefore(this.attachToMenu, document
                .getAnonymousElementByAttribute(this, "aid",
                    "addToMyCollections"));
            if (attachSlots.length > 1) {
                menu.appendChild(document.createElementNS(PencilNamespaces.xul,
                    "menupopup"));
            }
            for (var i = 0; i < attachSlots.length; i++) {
                var displayName = attachSlots[i].displayName;
                var tool = container.getProperty(attachSlots[i].name);
                if (tool && tool != "") {
                    displayName += " (" + tool.targetName + ")";
                }
                if (attachSlots.length == 1) {
                    this.attachToMenu.setAttribute("label", Util.getMessage(
                        "canvas.merge.label.1", displayName));
                    this.attachToMenu._container = container;
                    this.attachToMenu._attachSlot = attachSlots[0];
                } else {
                    var item = document.createElementNS(PencilNamespaces.xul,
                        "menuitem");
                    menu.firstChild.appendChild(item);
                    item.setAttribute("label", displayName);
                    item._container = container;
                    item._attachSlot = attachSlots[i];
                }
            }
            this.attachToMenu.addEventListener("command", function (event) {
                try {
                    var containerNode = Dom.findUpward(event.originalTarget,
                        function (node) {
                            return node._container;
                        });
                    var attachSlotNode = Dom.findUpward(event.originalTarget,
                        function (node) {
                            return node._attachSlot;
                        });
                    if (containerNode && containerNode._container && attachSlotNode
                        && attachSlotNode._attachSlot
                        && containerNode._container._target) {
                        var target = containerNode._container._target;
                        var defId = target.def.id;
                        var name = target.getName ? target.getName() : "";
                        var metaNode = Dom.getSingle("./p:metadata", target.svg);
                        var ctm = target.svg
                            .getTransformToElement(thiz.drawingLayer);
                        var cctm = containerNode._container.svg
                            .getTransformToElement(thiz.drawingLayer);
                        // var uuid = Util.newUUID();
                        // target.svg.setAttribute("id", uuid);
                        var uuid = target.svg.getAttribute("id");
                        containerNode._container.setProperty(
                            attachSlotNode._attachSlot.name, new Attachment(
                                defId, uuid, name, metaNode, ctm, cctm));
                        thiz.selectShape(containerNode._container.svg);
                    }
                } catch (e) {
                    Console.dumpError(e);
                }
            }, false);
        }

    };

    buildDetachMenuItem() {

        if (this.detachFromMenu) {
            this.popup.removeChild(this.detachFromMenu);
            this.detachFromMenu = null;
        }
        if (!this.currentController)
            return;
        if (this.currentController.canDetach && this.currentController.canDetach()) {
            var thiz = this;
            var container = this.currentController;
            this.detachFromMenu = document.createElementNS(PencilNamespaces.xul,
                "menu");
            this.detachFromMenu.setAttribute("label", Util
                .getMessage("canvas.split.label"));
            var attachedSlots = this.currentController.getAttachedSlots();
            if (attachedSlots.length == 1) {
                this.detachFromMenu = document.createElementNS(
                    PencilNamespaces.xul, "menuitem");
            }
            var menu = this.popup.insertBefore(this.detachFromMenu, document
                .getAnonymousElementByAttribute(this, "aid",
                    "addToMyCollections"));
            if (attachedSlots.length > 1) {
                menu.appendChild(document.createElementNS(PencilNamespaces.xul,
                    "menupopup"));
            }
            for (var i = 0; i < attachedSlots.length; i++) {
                var displayName = attachedSlots[i].displayName;
                var tool = container.getProperty(attachedSlots[i].name);
                if (tool && tool != "") {
                    displayName += " (" + tool.targetName + ")";
                }
                if (attachedSlots.length == 1) {
                    this.detachFromMenu.setAttribute("label", Util.getMessage(
                        "canvas.split.label.1", displayName));
                    this.detachFromMenu._container = container;
                    this.detachFromMenu._detachSlot = attachedSlots[0];
                } else {
                    var item = document.createElementNS(PencilNamespaces.xul,
                        "menuitem");
                    menu.firstChild.appendChild(item);
                    item.setAttribute("label", displayName);
                    item._container = container;
                    item._detachSlot = attachedSlots[i];
                }
            }
            this.detachFromMenu.addEventListener("command", function (event) {
                        try {
                            var containerNode = Dom.findUpward(
                                event.originalTarget, function (node) {
                                    return node._container;
                                });
                            var detachSlotNode = Dom.findUpward(
                                event.originalTarget, function (node) {
                                    return node._detachSlot;
                                });
                            if (containerNode && containerNode._container
                                && detachSlotNode
                                && detachSlotNode._detachSlot) {
                                var attachment = containerNode._container
                                    .getProperty(detachSlotNode._detachSlot.name);
                                if (attachment && attachment.defId) {
                                    var shapeDef = CollectionManager.shapeDefinition
                                        .locateDefinition(attachment.defId);
                                    if (shapeDef) {
                                        thiz.insertShapeImpl_(shapeDef,
                                            containerNode._container
                                                .getBounding());
                                        var controller = thiz.currentController;
                                        if (controller) {
                                            Svg.ensureCTM(controller.svg,
                                                attachment.ctm);
                                            var metaNode = Dom
                                                .parseToNode(attachment.metaData);
                                            if (metaNode) {
                                                Dom
                                                    .workOn(
                                                        "./p:property",
                                                        metaNode,
                                                        function (
                                                            node) {
                                                            var name = node
                                                                .getAttribute("name");
                                                            var value = Dom
                                                                .getText(node);
                                                            controller
                                                                .setProperty(
                                                                    name,
                                                                    value);
                                                        });
                                            }
                                            // var g =
                                            // document.getElementById(attachment.targetId);
                                            var octm = attachment.cctm;
                                            var cctm = containerNode._container.svg
                                                .getTransformToElement(thiz.drawingLayer);
                                            // var matrix =
                                            // containerNode._container.svg.createSVGTransform().matrix;;
                                            // matrix =
                                            // matrix.multiply(octm);
                                            // var ctm =
                                            // g.getTransformToElement(thiz.drawingLayer);
                                            // Svg.ensureCTM(controller.svg,
                                            // ctm);
                                            controller.moveBy(cctm.e
                                                - octm.e, cctm.f
                                                - octm.f);
                                        }
                                    }
                                    containerNode._container
                                        .setProperty(
                                            detachSlotNode._detachSlot.name,
                                            new Attachment(null,
                                                null, null));
                                    thiz
                                        .selectShape(containerNode._container.svg);
                                    thiz.invalidateEditors();
                                }
                            }
                        } catch (e) {
                            Console.dumpError(e);
                        }
                    }, false);
        }

    };

    insertEditorContextMenuItem(menuItem) {

        var parent = this.popupSeparator.parentNode;
        menuItem._isEditor = true;

        parent.insertBefore(menuItem, this.popupSeparator);

    };

    invalidateEditors(source?) {

        for (editor in this.onScreenEditors) {
            var e = this.onScreenEditors[editor];
            if (!source || source != e)
                e.invalidate();
        }

        // Pencil.invalidateSharedEditor();
        // invalidates all selections
        for (var i = 0; i < this.selectionContainer.childNodes.length; i++) {
            var rect = this.selectionContainer.childNodes[i];
            this._invalidateOneSelection(rect);
        }

    };

    _showPropertyDialog() {

        if (this.propertyPageEditor && this.currentController) {
            this.propertyPageEditor.showAndAttach(this.currentController);
        }

    };

    handlePropertyMenuItemCommand(event) {

        this._showPropertyDialog();

    };

    handleDblclick(event) {

        stencilDebug("handleDblclick, start finding top");

        // find the top, get the def
        var thiz = this;
        var top = Dom.findTop(event.originalTarget, function (node) {
            return node.hasAttributeNS
                && node.hasAttributeNS(PencilNamespaces.p, "type")
                && node.getAttributeNS(PencilNamespaces.p, "type") == "Shape"
                && !thiz.isShapeLocked(node);
        });

        stencilDebug("handleDblclick, top = " + top);

        if (!top)
            return;

        var target = this.createControllerFor(top);

        stencilDebug("emitting event p:ShapeDoubleClicked");
        Dom.emitEvent("p:ShapeDoubleClicked", top, {
            controller: target,
            origTarget: event.originalTarget,
            clientX: event.clientX,
            clientY: event.clientY
        });

    };

    focus() {

        // document.getElementById("richTextEditorToolbar").focus();
        // document.commandDispatcher.rewindFocus();
        // document.commandDispatcher.advanceFocus();
        this.focusableBox.focus();

    };

    doCopy() {

        if (!this.currentController)
            return;

        var transferableData = this.currentController.createTransferableData();

        transferableData.dataNode.removeAttribute("p:parentRef");
        var metaNode = Dom.getSingle(".//p:metadata", transferableData.dataNode);
        var childTargetsNode = Dom.getSingle("./p:childTargets", metaNode);
        if (childTargetsNode) {
            childTargetsNode.parentNode.removeChild(childTargetsNode);
        }

        // serialize to string
        var textualData = new XMLSerializer()
            .serializeToString(transferableData.dataNode);

        clipboard.writeText(textualData);
    };

    Canvas
.
    domParser = new DOMParser();

    doPaste(withAlternative) {
        this.useAlternativePasting = withAlternative ? true : false;
        var formats = clipboard.availableFormats();
        if (!formats) return;

        var contents = [];

        //the following implementation is electron-specific
        if (formats.indexOf(PlainTextXferHelper.MIME_TYPE) >= 0) {
            //in some cases, XML-based clipboard contents are transfered using plain text
            //we need to try parsing it first instead of directly assuming that the content is just plain text

            var text = clipboard.readText();
            if (text) {
                var parsed = false;

                function parseContent(string) {
                    var dom = Canvas.domParser.parseFromString(string, "text/xml");
                    if (dom) {
                        var node = dom.documentElement;
                        if (node.namespaceURI == PencilNamespaces.svg) {
                            if (node.localName == "g") {
                                var typeAttribute = node.getAttributeNS(PencilNamespaces.p, "type");
                                return {
                                    type: (typeAttribute == "Shape" || typeAttribute == "Group") ? ShapeXferHelper.MIME_TYPE : TargetSetXferHelper.MIME_TYPE,
                                    data: dom
                                };
                            } else if (node.localName == "svg") {
                                return {
                                    type: SVGXferHelper.MIME_TYPE,
                                    data: dom
                                };
                            }
                        }
                    }
                    return null;
                }

                var content = parseContent(text);
                if (content) {
                    contents.push(content);
                    parsed = true;
                }

                if (!parsed) {
                    var parsedFile = false;
                    var fileTypes = [".png", ".jpg", ".jpeg", ".gif", ".svg"];
                    var fileType = path.extname(text);
                    if (fileType && fileTypes.indexOf(fileType.toLowerCase()) >= 0) {
                        try {
                            var fstat = fsExistSync(text);
                            if (fstat && fstat.isFile()) {

                                if (fileType.toLowerCase() == ".png") {
                                    var image = nativeImage.createFromPath(text);
                                    if (image) {
                                        var id = Pencil.controller.nativeImageToRefSync(image);

                                        var size = image.getSize();

                                        contents.push({
                                            type: PNGImageXferHelper.MIME_TYPE,
                                            data: new ImageData(size.width, size.height, ImageData.idToRefString(id))
                                        });
                                        parsedFile = true;
                                    }
                                } else if (fileType.toLowerCase() == ".svg") {
                                    var fileContent = fs.readFileSync(text, "utf8");
                                    var svgContent = parseContent(fileContent);
                                    if (svgContent) {
                                        contents.push(svgContent);
                                        parsedFile = true;
                                    }
                                } else {
                                    contents.push({
                                        type: JPGGIFImageXferHelper.MIME_TYPE,
                                        data: text
                                    });
                                    parsedFile = true;
                                }
                            }

                        } catch (e) {
                            Console.dumpError(e);
                        }
                    }

                    if (!parsedFile) {
                        contents.push({
                            type: PlainTextXferHelper.MIME_TYPE,
                            data: text
                        });
                    }
                }
            }
        }

        if (formats.indexOf(RichTextXferHelper.MIME_TYPE) >= 0) {
            var html = clipboard.readHTML();
            if (html) {
                contents.push({
                    type: RichTextXferHelper.MIME_TYPE,
                    data: html
                });
            }
        }

        if (formats.indexOf(PNGImageXferHelper.MIME_TYPE) >= 0) {
            var image = clipboard.readImage();
            if (image) {
                var id = Pencil.controller.nativeImageToRefSync(image);
                var size = image.getSize();

                contents.push({
                    type: PNGImageXferHelper.MIME_TYPE,
                    data: new ImageData(size.width, size.height, ImageData.idToRefString(id))
                });
            }
        }

        //initial impl: use first win approach
        for (var j = 0; j < contents.length; j++) {
            var content = contents[j];
            var handled = false;

            for (var i in this.xferHelpers) {
                var helper = this.xferHelpers[i];

                if (helper.type == content.type) {
                    try {
                        helper.handleData(content.data);
                        handled = true;
                        break;
                    } catch (e) {
                        console.error(e);
                    }
                }
            }

            if (handled) break;
        }

    };

    handleMouseDown(event) {
        this._mouseDownAt = event.timeStamp;
        event.preventDefault();

        tick("begin");
        Dom.emitEvent("p:CanvasMouseDown", this.element, {});

        var canvasList = Pencil.getCanvasList();
        for (var i = 0; i < canvasList.length; i++) {
            if (canvasList[i] != this) {
                canvasList[i].selectNone();
            }
        }

        this.snappingHelper.clearSnappingGuide();

        this._button = event.button;

        var thiz = this;
        var isInControlLayer = Dom.findUpward(event.originalTarget, function (node) {
            return (node == thiz.controlLayer);
        });
        if (isInControlLayer) return;

        var top = Dom.findTop(event.originalTarget, function (node) {
            return node.hasAttributeNS
                && node.hasAttributeNS(PencilNamespaces.p, "type");
        });

        if (top && this.isShapeLocked(top)) top = null;

        if (!top) {
            this.lastTop = null;
            // this.clearSelection();
            // this._detachEditors();
            this.currentController = null;
            // this.setAttributeNS(PencilNamespaces.p, "p:selection", 0);

            this.isSelectingRange = true;
            this.lastMousePos = this.getEventLocation(event);
            this.setRangeBoundStart(this.lastMousePos.x, this.lastMousePos.y);
            this.setRangeBoundVisibility(true);
            this.setRangeBoundSize(0, 0);
            this.currentRange = {
                w: this.lastMousePos.x,
                y: this.lastMousePos.y,
                width: 0,
                height: 0
            };

            this._sayTargetChanged();
            this.endFormatPainter();

            return;
        }

        var controller = null;

        var targets = this.getSelectedTargets();

        var foundTarget = null;
        for (i in targets) {
            var target = targets[i];
            if (target.isFor(top)) {
                foundTarget = target;
                break;
            }
        }
        if (event.shiftKey) {
            if (!foundTarget && top) {
                thiz.clearSelection();
                thiz.addToSelection(thiz.createControllerFor(top));
            }
            this.duplicateMode = true;
            this.mouseUp = false;

            var thiz = this;
            this.duplicateFunc = function () {
                var target = thiz.currentController.createTransferableData();
                var contents = [];

                target.dataNode.removeAttribute("p:parentRef");
                var metaNode = Dom.getSingle(".//p:metadata", target.dataNode);
                var childTargetsNode = Dom.getSingle("./p:childTargets", metaNode);
                if (childTargetsNode) {
                    childTargetsNode.parentNode.removeChild(childTargetsNode);
                }

                // serialize to string
                var textualData = new XMLSerializer()
                    .serializeToString(target.dataNode);

                var dom = this.domParser.parseFromString(textualData, "text/xml");
                dom.copySamePlace = true;
                var node = dom.documentElement;
                if (node.namespaceURI == PencilNamespaces.svg) {
                    if (node.localName == "g") {
                        var typeAttribute = node.getAttributeNS(PencilNamespaces.p, "type");
                        contents.push({
                            type: (typeAttribute == "Shape" || typeAttribute == "Group") ? ShapeXferHelper.MIME_TYPE : TargetSetXferHelper.MIME_TYPE,
                            data: dom
                        });

                    } else if (node.localName == "svg") {
                        contents.push({
                            type: SVGXferHelper.MIME_TYPE,
                            data: dom
                        })
                    }
                }

                for (var j = 0; j < contents.length; j++) {
                    var content = contents[j];
                    var handled = false;

                    for (var i in thiz.xferHelpers) {
                        var helper = thiz.xferHelpers[i];

                        if (helper.type == content.type) {
                            try {
                                helper.handleData(content.data);
                                handled = true;
                                break;
                            } catch (e) {
                                console.error(e);
                            }
                        }
                    }
                    if (handled) break;
                }
                thiz.controllerHeld = true;

                thiz.oX = Math.round(event.clientX / thiz.zoom);
                thiz.oY = Math.round(event.clientY / thiz.zoom);

                thiz._lastNewX = Math.round(event.clientX / thiz.zoom);
                thiz._lastNewY = Math.round(event.clientY / thiz.zoom);

                thiz.oldPos = thiz.currentController.getGeometry();

                tick("before setPositionSnapshot");
                thiz.currentController.setPositionSnapshot();
                tick("after setPositionSnapshot");

                thiz.duplicateFunc = null;
            }
        } else if (this.isEventWithControl(event)) {
            if (foundTarget) {
                this.removeFromSelection(foundTarget);
            } else {
                var newController = this.createControllerFor(top);
                this.addToSelection(newController);
            }
        } else {
            if (!foundTarget || targets.length == 1) {
                this.clearSelection();
                controller = this.createControllerFor(top);
                this.addToSelection(controller);
            }
        }

        var targets = this.getSelectedTargets();
        if (targets.length > 1) {
            controller = new TargetSet(this, targets);
        } else if (targets.length == 1 && controller == null) {
            controller = targets[0];
        }

        this.setAttributeNS(PencilNamespaces.p, "p:selection", targets.length);

        if (controller) {
            try {
                this.currentController = controller;
                this.controllerHeld = true;

                this.oX = Math.round(event.clientX / this.zoom);
                this.oY = Math.round(event.clientY / this.zoom);

                this._lastNewX = Math.round(event.clientX / this.zoom);
                this._lastNewY = Math.round(event.clientY / this.zoom);

                this.oldPos = this.currentController.getGeometry();

                tick("before setPositionSnapshot");
                this.currentController.setPositionSnapshot();
                tick("after setPositionSnapshot");

                if (event.button == 0)
                    this.setAttributeNS(PencilNamespaces.p, "p:holding", "true");

                if (top != this.lastTop || event.ctrlKey || event.button != 0) {
                    this.reClick = false;
                    this._attachEditors(controller);
                } else {
                    if (event.detail != 2)
                        this.reClick = true;
                }

                this.hasMoved = false;
                this.lastTop = top;
                this._sayTargetChanged();
                tick("done");
            } catch (e) {
                Console.dumpError(e);
            }
        }

    };

    isEventWithControl(event) {
        return (event.ctrlKey && !IS_MAC) || (event.metaKey && IS_MAC);
    };

    doGroup() {

        this.run(this.doGroupImpl_, this, Util.getMessage("action.group.shapes"));

    };

    doGroupImpl_() {

        var targets = this.getSelectedTargets();
        if (targets.length <= 1)
            return;

        // create an svg:g to box all the selected target
        var g = this.ownerDocument.createElementNS(PencilNamespaces.svg, "g");
        g.setAttributeNS(PencilNamespaces.p, "p:type", "Group");

        var nodes = [];
        var rect = null;
        Dom.workOn("./svg:g[@p:type]", this.drawingLayer, function (node) {
            for (t in targets) {
                if (targets[t].isFor(node)) {
                    nodes.push(node);
                    var childRect = targets[t].getBounding();
                    rect = rect ? Svg.joinRect(rect, childRect) : childRect;
                }
            }
        });

        for (n in nodes) {
            var node = nodes[n];
            node.parentNode.removeChild(node);
            g.appendChild(node);
        }

        this.drawingLayer.appendChild(g);

        for (t in targets) {
            targets[t].moveBy(0 - rect.x, 0 - rect.y, true);
        }

        g.setAttribute("transform", "translate(" + [rect.x, rect.y] + ")");

        this.selectShape(g);

        this.currentController.processNewGroup();

        this.snappingHelper.updateSnappingGuide(this.currentController);
        for (var t in this.currentController.targets) {
            this.snappingHelper.updateSnappingGuide(
                this.currentController.targets[t], true);
        }

    };

    ensureControllerInView() {

    };

    doUnGroup() {

        this.run(this.doUnGroupImpl_, this, Util
            .getMessage("action.ungroup.shapes"), null);

    };

    doUnGroupImpl_() {

        if (!this.currentController)
            return;
        if (this.currentController.constructor != Group)
            return;

        // remove group
        this.snappingHelper.updateSnappingGuide(this.currentController, true);

        var nodes = this.currentController.ungroup();

        this.selectMultiple(nodes);

        if (this.currentController.constructor == TargetSet) {
            for (var t in this.currentController.targets) {
                this.snappingHelper
                    .updateSnappingGuide(this.currentController.targets[t]);
            }
        } else {
            this.snappingHelper.updateSnappingGuide(this.currentController);
        }

    };

    isShapeLocked(shape) {

        return shape.getAttributeNS(PencilNamespaces.p, "locked") == "true";

    };

    toggleLocking() {

        this.run(this.toggleLockingImpl_, this, Util.getMessage("action.lock.shape"));

    };

    toggleLockingImpl_() {
        if (!this.lockingStatus)
            return;
        if (this.lockingStatus.controller && this.lockingStatus.controller.lock) {
            this.lockingStatus.controller.lock();
            this.selectNone();
        } else if (this.lockingStatus.node) {
            this.lockingStatus.node.removeAttributeNS(PencilNamespaces.p, "locked");
            this.selectShape(this.lockingStatus.node);
        }

    };

    deleteSelected() {

        this.run(this.deleteSelectedImpl_, this, Util
            .getMessage("action.delete.shape"));

    };

    deleteSelectedImpl_() {

        // this.snappingHelper.updateSnappingGuide(this.currentController, true);
        this.currentController.deleteTarget();
        this.currentController = null;
        this._detachEditors();
        this.clearSelection();
        this._sayTargetChanged();

    };

    _sayContentModified() {

        Dom.emitEvent("p:ContentModified", this.element);

    };

    _saveMemento(actionName) {

        this.careTaker.save(actionName);

    };

    getMemento(actionName) {
        return new CanvasMemento(this.drawingLayer.cloneNode(true), {
            width: this.width,
            height: this.height
        }, actionName);
    };

    setMemento(memento) {

        this.selectNone();
        Dom.empty(this.drawingLayer);

        // alert("copy back: " + memento.node.childNodes.length + " nodes");
        var fragment = this.drawingLayer.ownerDocument.createDocumentFragment();
        for (var i = 0; i < memento.node.childNodes.length; i++) {
            fragment.appendChild(this.drawingLayer.ownerDocument.importNode(
                memento.node.childNodes[i], true));
        }
        this.drawingLayer.appendChild(fragment);

        this.focusableBox.style.visibility = "hidden";
        this.focusableBox.style.visibility = "visible";

        var width = parseInt(memento.metadata.width, 10) || this.width;
        var height = parseInt(memento.metadata.height, 10) || this.height;

        if (this.width != width || this.height != height) {
            this.setSizeImpl_(width, height);
        }

        this._sayContentModified();

    };

    run(job, targetObject, actionName, args?) {
        try {
            // console.log();
            job.apply(targetObject, args);
        } catch (e) {
            Console.dumpError(e);
        } finally {
            this._saveMemento(actionName);
            if (Pencil.controller && !Pencil.controller.activePageLoading) this._sayContentModified();
        }

    };

    getCanvasState() {
        var state = {
            zoom: this.zoom,
        }

        if (this._scrollPane) {
            state.scrollTop = this._scrollPane.scrollTop;
            state.scrollLeft = this._scrollPane.scrollLeft;
        }

        return state;
    };

    setCanvasState(state) {
        if (state) {
            this.zoomTo(state.zoom);
            if (this._scrollPane) {
                window.setTimeout(function () {
                    this._scrollPane.scrollTop = state.scrollTop;
                    this._scrollPane.scrollLeft = state.scrollLeft;
                }.bind(this), 10);
            }
        } else {
            this.zoomTo(1);
            if (this._scrollPane) {
                window.setTimeout(function () {
                    this._scrollPane.scrollTop = 0;
                    this._scrollPane.scrollLeft = 0;
                }.bind(this), 10);
            }
        }
    };

    setBackgroundColor(color) {
        if (color) {
            this.element.style.backgroundColor = color.toRGBAString();
        } else {
            this.element.style.backgroundColor = "";
        }


    };

    setSize(width, height) {
        this.run(this.setSizeImpl_, this, Util.getMessage(
            "action.canvas.resize"), [width, height]);
    };

    setSizeImpl_(width, height) {
        var thiz = this;

        // this.run()
        this.width = width;
        this.height = height;

        this.svg.setAttribute("width", 10);
        this.svg.setAttribute("height", 10);

        window.setTimeout(function () {
            thiz.svg.setAttribute("width", Math.ceil(thiz.width * thiz.zoom));
            thiz.svg.setAttribute("height", Math.ceil(thiz.height * thiz.zoom));

            CanvasImpl.setupGrid.apply(thiz);
        }, 50);

        Dom.emitEvent("p:SizeChanged", this.element, {
            canvas: this
        });

        this.snappingHelper.rebuildSnappingGuide();
    };

    setBackgroundImageData(image, dimBackground) {

        if (!image) {
            this.backgroundImage.setAttributeNS(PencilNamespaces.xlink,
                "xlink:href", "");
            this.backgroundImage.style.display = "none";
            this.hasBackgroundImage = false;
            this.removeAttributeNS(PencilNamespaces.p, "with-background");
            return;
        }

        this.backgroundImage.setAttributeNS(PencilNamespaces.xlink, "xlink:href",
            image.url);
        this.backgroundImage.setAttribute("width", image.width);
        this.backgroundImage.setAttribute("height", image.height);
        this.backgroundImage.style.display = "";
        this.hasBackgroundImage = true;
        this.setAttributeNS(PencilNamespaces.p, "p:with-background", "true");
        if (!dimBackground) {
            this.removeAttributeNS(PencilNamespaces.p, "with-background-dimmer");
        } else {
            this.setAttributeNS(PencilNamespaces.p, "p:with-background-dimmer",
                "true");
        }

    };

    setDimBackground(dimBackground) {

        if (!dimBackground) {
            this.removeAttributeNS(PencilNamespaces.p, "with-background-dimmer");
        } else {
            this.setAttributeNS(PencilNamespaces.p, "p:with-background-dimmer",
                "true");
        }

    };

    sizeToContent(hPadding, vPadding) {
        var newSize = null;
        var thiz = this;
        this.run(function () {
            newSize = thiz.sizeToContent__(hPadding, vPadding);
        }, this, Util.getMessage("action.canvas.resize"));
        return newSize;
    };

    sizeToContent__(hPadding, vPadding) {
        var pageMargin = Pencil.controller.getDocumentPageMargin() || 0;

        hPadding += pageMargin;
        vPadding += pageMargin;

        this.zoomTo(1.0);

        var thiz = this;
        var maxBox = null;

        function out(name, rect) {
            console.log(name, "left: ", rect.left, "top: ", rect.top, "width: ", rect.width, "height: ", rect.height);
        }

        out("this.svg", this.svg.getBoundingClientRect());
        Dom.workOn("./svg:g[@p:type]", this.drawingLayer, function (node) {
            try {
                var controller = thiz.createControllerFor(node);
                if (controller.def && controller.def.meta.excludeSizeCalculation) return;
                var bbox = controller.getBoundingRect();
                //HACK: inspect the strokeWidth attribute to fix half stroke bbox issue
                var box = {
                    x1: bbox.x,
                    y1: bbox.y,
                    x2: bbox.x + bbox.width,
                    y2: bbox.y + bbox.height
                };

                if (controller.getGeometry) {
                    var geo = controller.getGeometry();
                    if (geo.ctm && geo.ctm.a == 1 && geo.ctm.b == 0 && geo.ctm.c == 0 && geo.ctm.d == 1 && geo.dim) {
                        box.x1 = geo.ctm.e;
                        box.y1 = geo.ctm.f;
                        box.x2 = box.x1 + geo.dim.w;
                        box.y2 = box.y1 + geo.dim.h;
                    }
                }

                if (maxBox == null) {
                    maxBox = box;
                } else {
                    maxBox = {
                        x1: Math.min(maxBox.x1, box.x1),
                        y1: Math.min(maxBox.y1, box.y1),
                        x2: Math.max(maxBox.x2, box.x2),
                        y2: Math.max(maxBox.y2, box.y2)
                    };
                }
            } catch (e) {
                Util.error(Util.getMessage("error.title"), e, Util
                    .getMessage("button.cancel.close"));
            }
        });
        if (maxBox == null) {
            Util.info(Util.getMessage("error.title"), Util
                    .getMessage("the.current.document.is.empty.sizing.is.failed"),
                Util.getMessage("button.cancel.close"));
            return;
        }
        maxBox.x1 = Math.floor(maxBox.x1);
        maxBox.x2 = Math.ceil(maxBox.x2);
        maxBox.y1 = Math.floor(maxBox.y1);
        maxBox.y2 = Math.ceil(maxBox.y2);

        var width = maxBox.x2 - maxBox.x1 + 2 * hPadding;
        var height = maxBox.y2 - maxBox.y1 + 2 * vPadding;

        // moving
        var dx = 0 - maxBox.x1 + hPadding;
        var dy = 0 - maxBox.y1 + vPadding;

        Dom.workOn("./svg:g[@p:type]", this.drawingLayer, function (node) {
            try {
                var controller = thiz.createControllerFor(node);
                var bbox = controller.getBoundingRect();
                controller.moveBy(dx, dy);
            } catch (e) {
                alert(e);
            }
        });

        this.setSizeImpl_(width, height);

        this.selectNone();

        return {
            width: width,
            height: height
        };
    };

    addSelectedToMyCollection() {
        if (!this.currentController) return;

        var data = {
            collections: PrivateCollectionManager.privateShapeDef.collections,
            valueHolder: {}
        };

        var run = function (data) {
            try {
                var target = Pencil.getCurrentTarget();
                // generating text/xml+svg
                var svg = target.svg.cloneNode(true);

                var fakeDom = Controller.parser.parseFromString("<Document xmlns=\"" + PencilNamespaces.p + "\"></Document>", "text/xml");
                fakeDom.documentElement.appendChild(svg);

                Pencil.controller.prepareForEmbedding(fakeDom, function () {
                    fakeDom.documentElement.removeChild(svg);
                    var valueHolder = data.valueHolder;
                    if (!valueHolder.shapeName)
                        return;

                    var shapeDef = new PrivateShapeDef();
                    shapeDef.displayName = valueHolder.shapeName;
                    shapeDef.content = svg;
                    shapeDef.id = shapeDef.displayName.replace(/\s+/g, "_").toLowerCase()
                        + "_" + (new Date()).getTime();

                    var collection = valueHolder.collection;
                    var isNewCollection = (collection == null || collection == -1);

                    if (isNewCollection) {
                        // debug("creating new collection...");
                        collection = new PrivateCollection();
                        collection.displayName = valueHolder.collectionName;
                        collection.description = valueHolder.collectionDescription;
                        collection.id = collection.displayName.replace(/\s+/g, "_")
                                .toLowerCase()
                            + "_" + (new Date()).getTime();

                        collection.shapeDefs.push(shapeDef);
                    }
                    Config.set("PrivateCollection.lastUsedCollection.id", collection.id);
                    Config.set("PrivateCollection.lastSelectCollection.id", collection.id);
                    debug("generating icon... :", valueHolder.autoGenerateIcon);
                    if (valueHolder.autoGenerateIcon) {
                        Util.generateIcon(target, 64, 64, 2, null, function (icondata) {
                            debug("\t done generating icon.");
                            shapeDef.iconData = icondata;
                            if (isNewCollection) {
                                PrivateCollectionManager.addShapeCollection(collection);
                            } else {
                                PrivateCollectionManager.addShapeToCollection(collection,
                                    shapeDef);
                            }
                            return;
                        });
                    } else {
                        var filePath = valueHolder.shapeIcon;
                        var image = nativeImage.createFromPath(filePath);

                        shapeDef.iconData = image.toDataURL();
                        if (isNewCollection) {
                            PrivateCollectionManager.addShapeCollection(collection);
                        } else {
                            PrivateCollectionManager.addShapeToCollection(collection,
                                shapeDef);
                        }

                    }
                });

            } catch (e) {
                Console.dumpError(e);
            }
        };

        var myCollectionDialog = new PrivateCollectionDialog();
        myCollectionDialog.open({
            valueHolder: data.valueHolder,
            onDone: function (data) {
                data.valueHolder = data;
                run(data);
            }
        });
    };

    insertPrivateShape(shapeDef, bound) {

        this.run(this.insertPrivateShapeImpl_, this, Util.getMessage(
            "action.create.shape", shapeDef.displayName), [shapeDef,
            bound ? bound : null]);

    };

    insertPrivateShapeImpl_(shapeDef, bound) {

        // debug("inserting private shape: " + shapeDef.displayName + ", id: " +
        // shapeDef.id);
        var g = this.ownerDocument.createElementNS(PencilNamespaces.svg, "g");
        g.appendChild(shapeDef.content);

        // validate
        var shape = Dom.getSingle("/svg:g[@p:type='Shape' or @p:type='Group']", g);
        if (!shape) {
            return;
        }

        shape = this.ownerDocument.importNode(shape, true);

        // generate the ids
        Dom.workOn(".//*[@p:name]", shape, function (node) {
            var name = node.getAttributeNS(PencilNamespaces.p, "name");
            var oldId = node.getAttribute("id");
            if (oldId)
                return;

            var uuid = Util.newUUID();
            node.setAttribute("id", uuid);
            node.id = uuid;

            Dom.updateIdRef(shape, name, uuid);
        });

        Dom.renewId(shape);

        shape.style.visibility = "hidden";
        this.setAttributeNS(PencilNamespaces.p, "p:holding", "true");

        this.drawingLayer.appendChild(shape);

        Pencil.controller.invalidateContentNode(shape, function () {
            this.selectShape(shape);
            if (this.currentController.validateAll) this.currentController.validateAll();
            if (bound) {
                var bbox = this.currentController.getBounding();
                //note: the returned bbox is NOT zoomed
                this.currentController.moveBy(
                    Math.round(bound.x - bbox.x - bbox.width / 2),
                    Math.round(bound.y - bbox.y - bbox.height / 2),
                    true);
            }
            shape.style.visibility = "visible";
            this.removeAttributeNS(PencilNamespaces.p, "holding");

            this.ensureControllerInView();
            this.snappingHelper.updateSnappingGuide(this.currentController);
            DockingManager.enableDocking(this.currentController);

            this.invalidateEditors();
        }.bind(this));
    };

    endFormatPainter() {
        Pencil._painterSourceTarget = null;
        Pencil._painterSourceProperties = null;
        Pencil.setPainterCommandChecked(false);
        return true;

    };

    isFormatPainterAvailable() {

        return Pencil._painterSourceTarget && Pencil._painterSourceProperties;

    };

    getPainterPropertyMap() {

        var ppm = Config.get("painterPropertyMap");
        if (ppm == null || ppm == "") {
            ppm = "fillColor,foreColor,textColor,strokeColor,borderColor,"
                + "strokeStyle,textAlign,textFont,shadowStyle,shadowColor,"
                + "withBlur,withShadow,textPadding,startFillColor,endFillColor"
            Config.set("painterPropertyMap", ppm);
        }
        return ppm.split(",");

    };

    startFakeMove(event) {

        this._button = 0;
        this.currentController = this.getSelectedTargets()[0];
        this.controllerHeld = true;

        this.oX = Math.round(event.clientX / this.zoom);
        this.oY = Math.round(event.clientY / this.zoom);
        this.oldPos = this.currentController.getGeometry();

        this.currentController.setPositionSnapshot();

//    OnScreenTextEditor._hide();

        this.setAttributeNS(PencilNamespaces.p, "p:holding", "true");
        this.reClick = false;

        this.hasMoved = false;
        this.lastTop = top;
        this._sayTargetChanged();

    };

    checkDnDEventTimestamp(event) {

        var now = new Date().getTime();
        if (!this.lastDragEnterExitEventTS) {
            this.lastDragEnterExitEventTS = now;
            return true;
        }
        var delta = now - this.lastDragEnterExitEventTS;

        // debug("Even ts delta: " + delta);

        this.lastDragEnterExitEventTS = now;

        return delta > 500;


    };

    __dragenter(event) {
        if (event.target == this.dragOverlay) return;
        if (this.element.getAttribute("is-dragover") == "true") return;
        this.element.setAttribute("is-dragover", "true");

        // debug("PCanvas: drag enter, " + event.originalTarget.localName);

        // find the first DO that accepts this drag session, use its for subsequence
        // events
        // in the same session

        this.currentDragObserver = null;
        for (var i in this.dragObservers) {
            var observer = this.dragObservers[i];
            try {
                var accepted = nsDragAndDrop.dragEnter(event, observer);
                if (accepted) {
                    this.currentDragObserver = observer;
                    break;
                }
            } catch (e) {
                Console.dumpError(e);
            }
        }
    };

    __dragleave(event) {
        // this.element.removeAttribute("p:selection");
        if (!this.currentDragObserver)
            return;
        try {
            nsDragAndDrop.dragExit(event, this.currentDragObserver);
        } catch (e) {
            Console.dumpError(e);
        }
        this.element.removeAttribute("is-dragover");
        this.element.removeAttribute("p:holding");
    };

    __dragend(event) {
        this.element.removeAttribute("is-dragover");
        // debug("drag end...");

    };

    __dragover(event) {
        if (!this.currentDragObserver)
            return;

        try {
            nsDragAndDrop.dragOver(event, this.currentDragObserver);
        } catch (e) {
            Console.dumpError(e);
        }

        /*
         *
         *
         * if (!this.currentDragProcessed) { tick("drag over");
         * this.lastAcceptedDragObserver = null; for (var i in this.dragObservers) {
         * var observer = this.dragObservers[i]; try { var accepted =
         * nsDragAndDrop.dragOver(event, observer); if (accepted) {
         * this.lastAcceptedDragObserver = observer; break; } } catch (e) {
         * Console.dumpError(e); } }
         *
         * this.currentDragProcessed = true; }
         *
         * if (this.hasDrag) { if (event.clientX != this._lastScreenX ||
         * event.clientY != this._lastScreenY) { this.handleMouseMove(event,
         * "fake"); this._lastScreenX = event.clientX; this._lastScreenY =
         * event.clientY; }
         *  }
         */

    };

    __drop(event) {
        var thiz = this;
        var data = event.dataTransfer.getData("collectionId");
        var data = nsDragAndDrop.getData("collectionId");
        var collections = CollectionManager.shapeDefinition.collections;
        // for (var i = 0; i < collections.length; i ++) {
        //     if (collections[i].id == data) {
        //         var count = CollectionManager.getCollectionUsage(collections[i]);
        //         count++;
        //         CollectionManager.setCollectionUsage(collections[i], count);
        //     }
        // }
        this.element.removeAttribute("is-dragover");
        if (this.canvasContentModifiedListener) {
            this.canvasContentModifiedListener(thiz);
            this.canvasContentModifiedListener = null;
        }
        if (!this.currentDragObserver) {
            return;
        }

        try {
            nsDragAndDrop.drop(event, this.currentDragObserver);
        } catch (e) {
            Console.dumpError(e);
        }
    };
}


SVGElement.prototype.getTransformToElement = SVGElement.prototype.getTransformToElement || function (elem) {
    return elem.getScreenCTM().inverse().multiply(this.getScreenCTM());
};

Object.defineProperty(Canvas.prototype, "ownerDocument", {
    get: function () {
        return this.element.ownerDocument;
    }
});

